/*
 * Copyright (c) 2009 the original author or authors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.as3undohistory.manage
{
	import com.as3undohistory.register.IObjectRegistration;
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.utils.Dictionary;
	import flash.utils.getDefinitionByName;
	import flash.utils.getQualifiedClassName;

	/**
	 * @inheritDoc 
	 * @author Roland Zwaga
	 * 
	 */
	public class UndoHistoryManager extends EventDispatcher implements IUndoHistoryManager
	{
		protected var objectPropertyStackLinks:Dictionary;
		protected var undoHistory:Array;
		protected var currentStack:Array;
		
		/**
		 * Creates a new instance of UndoHistoryManager
		 * @param objectRegistration
		 * @param target
		 * 
		 */
		public function UndoHistoryManager(objectRegistration:IObjectRegistration, target:IEventDispatcher=null)
		{
			super(target);
			_objectRegistration = objectRegistration;
			objectPropertyStackLinks = new Dictionary(true);
			undoHistory = new Array();
			currentStack = undoHistory;
		}
		
		private var _objectRegistration:IObjectRegistration;
		/**
		 * 
		 * @inheritDoc
		 * 
		 */
		public function get objectRegistration():IObjectRegistration
		{
			return _objectRegistration;
		}
		
		/**
		 * 
		 * @inheritDoc
		 * 
		 */
		public function addObject(instance:Object):void
		{
			var cls:Class = getDefinitionByName(getQualifiedClassName(instance)) as Class;
			if (_objectRegistration.isClassRegistered(cls) == false)
			{
				_objectRegistration.registerClass(cls);
			}
			var propertyStacks:Array = _objectRegistration.registerInstance(instance);
			bindProperties(propertyStacks);
		}
		
		/**
		 * @inheritDoc 
		 * 
		 */
		public function removeObject(instance:Object):void
		{ 
			var accessorPropertyStackLinks:Dictionary = objectPropertyStackLinks[instance] as Dictionary;
			for (var name:String in accessorPropertyStackLinks)
			{
				var propertyStack:IPropertyValueStack = (accessorPropertyStackLinks[name] as IPropertyValueStack);
				if (propertyStack.instance === instance)
				{
					propertyStack.instance = null;
					delete accessorPropertyStackLinks[name];
				}
			}
			objectPropertyStackLinks[instance] = null;
			delete objectPropertyStackLinks[instance];
		}
		
		/**
		 * @inheritDoc 
		 * 
		 */
		public function undo():void
		{
			//just to be sure:
			endTransaction();
			var item:Object = undoHistory.pop();
			var propStack:IPropertyValueStack =  item as IPropertyValueStack;
			if (propStack != null)
			{
				if (propStack.instance != null)
				{
					doUndo(propStack);
				}
				else
				{
					undo();
				}
			}
			else
			{
				var propStacks:Array =  item as Array;
				if (propStacks != null)
				{
					for(var i:int = propStacks.length-1; i > -1;i--)
					{
						var stack:IPropertyValueStack = propStacks[i] as IPropertyValueStack;
						if (stack.instance != null)
						{
							doUndo(stack);
						}
					}
				}
			}
		}
		
		protected function doUndo(propertyValueStack:IPropertyValueStack):void
		{
			var value:* = propertyValueStack.pop();
			try
			{
				//remove eventlistener to prevent the undohistorymanager to record
				//its own changes:
				unbindProperty(propertyValueStack);
				propertyValueStack.instance[propertyValueStack.accessorRegistration.accessorName] = value;
			}
			finally
			{
				bindProperty(propertyValueStack);
			}
		}
		
		/**
		 * @inheritDoc 
		 * 
		 */
		public function clear(clearRegistration:Boolean = false):void
		{
			undoHistory.length = 0;
			if (clearRegistration)
			{
				objectRegistration.clear();
			}
		}
		
		/**
		 * @inheritDoc 
		 * 
		 */
		public function startTransaction():void
		{
			var newStack:Array = new Array();
			undoHistory.push(newStack);
			currentStack = newStack;
		}
		
		/**
		 * @inheritDoc 
		 * 
		 */
		public function endTransaction():void
		{
			currentStack = undoHistory;
		}
		
		protected function bindProperties(propertyStacks:Array):void
		{
			propertyStacks.forEach(function(item:IPropertyValueStack,index:int,arr:Array):void
			{
				bindProperty(item);
			});
		}
		
		protected function unbindProperty(propertryStack:IPropertyValueStack):void
		{
			(propertryStack.instance as IEventDispatcher).removeEventListener(propertryStack.accessorRegistration.changeEventName,handlePropertyChange);
		}
		
		protected function bindProperty(propertryStack:IPropertyValueStack):void
		{
			var accessorPropertyStackLinks:Dictionary = objectPropertyStackLinks[propertryStack.instance] as Dictionary;
			if (accessorPropertyStackLinks == null)
			{
				accessorPropertyStackLinks = new Dictionary();
				objectPropertyStackLinks[propertryStack.instance] = accessorPropertyStackLinks;
			}
			accessorPropertyStackLinks[propertryStack.accessorRegistration.changeEventName] = propertryStack;
			(propertryStack.instance as IEventDispatcher).addEventListener(propertryStack.accessorRegistration.changeEventName,handlePropertyChange,false,0,true);
		}
		
		protected function handlePropertyChange(event:Event):void
		{
			var accessorPropertyStackLinks:Dictionary = objectPropertyStackLinks[event.target] as Dictionary;
			var propertyStack:IPropertyValueStack = accessorPropertyStackLinks[event.type];
			if (propertyStack != null)
			{
				propertyStack.push(propertyStack.instance[propertyStack.accessorRegistration.accessorName]);
				currentStack.push(propertyStack);
			}
		}
		
	}
}